﻿/// <reference path="Definitions/ws.d.ts" />
/// <reference path="Definitions/mysql.d.ts" />
/// <reference path="Definitions/restify.d.ts" />
/// <reference path="../mapparola.core/mapparolaCore.d.ts" />
/// <reference path="Definitions/linq.d.ts" />

import types = require('./sequelize-types');
import models = require('./sequelize-models');
import restify = require('restify');

var cookieParser = require('restify-cookies');
var cookie = require('cookie');
var core: typeof Mapparola.Core = require('../mapparola.core/mapparolaCore');
var Enumerable: linqjs.Enumerable = require('linq');

class ServerApp {
    private server: restify.Server;

    constructor() {
        //Connecting to database and initalizing sequelize
        models.initialize("mapparola_model", "root", "secret",
            {
                host: "localhost",
                port: 3306,
                dialect: 'mysql'
            });

        this.InitializeRestServer(this.server, "Mapparola REST endpoint", 8080);
    }

    private InitializeRestServer(server: restify.Server, serverName: string, port: number) {

        server = restify.createServer({
            name: serverName
        });

        server.use(restify.queryParser());
        server.use(restify.CORS({ credentials: true }));
        server.use(restify.fullResponse());
        server.use(restify.bodyParser({ mapParams: false }));
        server.use(cookieParser.parse);

        //Initializes REST endpoints
        var restEndPoints = [];

        /**
        Discussion related REST endpoints
        **/

        // Gets all the personal discussions belonging to a given user
        var personalDiscussionsGet = new core.PersonalDiscussionsGet();

        personalDiscussionsGet._serverProperties.callback =
        (req: restify.Request, res: restify.Response, next: restify.Next) => {
            var accountToken = this.getTokenFromSession(req);
            if (this.isAuthorized(accountToken)) {
                personalDiscussionsGet._serverProperties.res = res;
                personalDiscussionsGet._serverProperties.req = req;
                this.GetAllPersonalMessages(accountToken, res);
            } else {
                res.send(401);
            }
        };

        restEndPoints.push(personalDiscussionsGet);

        // Add a given discussion to the list of personal discussions
        var personalDiscussionPost = new core.PersonalDiscussionPost();

        personalDiscussionPost._serverProperties.callback =
        (req: restify.Request, res: restify.Response, next: restify.Next) => {
            var accountToken = this.getTokenFromSession(req);
            if (this.isAuthorized(accountToken)) {
                personalDiscussionPost._serverProperties.res = res;
                personalDiscussionPost._serverProperties.req = req;
                personalDiscussionPost.param = req.body;
                this.AddToPersonalDiscussions(personalDiscussionPost.param, accountToken, res);
            } else {
                res.send(401);
            }
        };

        restEndPoints.push(personalDiscussionPost);

        // Saves a discussion posted by a user
        var discussionPost = new core.DiscussionPost();

        discussionPost._serverProperties.callback =
        (req: restify.Request, res: restify.Response, next: restify.Next) => {
            var accountToken = this.getTokenFromSession(req);
            if (this.isAuthorized(accountToken)) {
                discussionPost._serverProperties.res = res;
                discussionPost._serverProperties.req = req;
                discussionPost.param = req.body;
                this.SaveDiscussion(discussionPost.param, accountToken, res);
            } else {
                res.send(410);
            }
        };

        restEndPoints.push(discussionPost);

        // Saves a discussion viewed by a user
        var viewedDiscussionPost = new core.ViewedDiscussionPost();

        viewedDiscussionPost._serverProperties.callback =
        (req: restify.Request, res: restify.Response, next: restify.Next) => {
            var accountToken = this.getTokenFromSession(req);
            if (this.isAuthorized(accountToken)) {
                viewedDiscussionPost._serverProperties.res = res;
                viewedDiscussionPost._serverProperties.req = req;
                viewedDiscussionPost.param = req.body;
                this.AddToUserViewedDiscussions(viewedDiscussionPost.param, accountToken, res);
            } else {
                res.send(401);
            }
        };

        restEndPoints.push(viewedDiscussionPost);

        // Gets all the new messages for a given user
        var newDiscussionsGet = new core.NewDiscussionsGet();

        newDiscussionsGet._serverProperties.callback =
        (req: restify.Request, res: restify.Response, next: restify.Next) => {
            var accountToken = this.getTokenFromSession(req);
            if (this.isAuthorized(accountToken)) {
                newDiscussionsGet._serverProperties.res = res;
                newDiscussionsGet._serverProperties.req = req;

                var data = req.params.data;
                if (data) {
                    newDiscussionsGet.param = JSON.parse(data);
                    this.GetUserNewMessagesAroundPosition(newDiscussionsGet.param, accountToken, res);
                } else {
                    res.send(400);
                }
            }
        };

        restEndPoints.push(newDiscussionsGet);

        /**
        Authentication related REST endpoints
        **/

        // Authenticates a user
        var authenticationPost = new core.AuthenticationPost();

        authenticationPost._serverProperties.callback =
        (req: restify.Request, res: restify.Response, next: restify.Next) => {
            if (this.isAuthorized(this.getTokenFromSession(req))) {
                res.send(200);
            } else {
                authenticationPost._serverProperties.res = res;
                authenticationPost._serverProperties.req = req;
                authenticationPost.param = req.body;
                this.LogUserIn(authenticationPost.param, res);
            }
        };

        restEndPoints.push(authenticationPost);

        // Gets the current logged user
        var authenticationGet = new core.AuthenticationGet();

        authenticationGet._serverProperties.callback =
        (req: restify.Request, res: restify.Response, next: restify.Next) => {
            var currentSessionToken = this.getTokenFromSession(req);

            if (currentSessionToken) {
                this.getUserFromToken(
                    currentSessionToken,
                    (user: Mapparola.Core.User) => {
                        res.send(200, user)
                    },
                    () => {
                        res.send(401);
                    });
            } else {
                res.send(401);
            }
        };

        restEndPoints.push(authenticationGet);

        /**
        End REST endpoint creation
        **/

        //initalizes all the REST endpoints on the current server
        console.log("Initializing " + restEndPoints.length + " REST endpoints");

        restEndPoints.forEach(
            (restEndPoint: any) => {
                switch (<Mapparola.Core.RestType>restEndPoint.type) {
                    case Mapparola.Core.RestType.GET:
                        console.log("GET: " + restEndPoint._serverProperties.partialUrl);
                        server.get(restEndPoint._serverProperties.partialUrl, restEndPoint._serverProperties.callback);
                        break;
                    case Mapparola.Core.RestType.POST:
                        console.log("POST: " + restEndPoint._serverProperties.partialUrl);
                        server.post(restEndPoint._serverProperties.partialUrl, restEndPoint._serverProperties.callback);
                        break;
                }
            });

        //TODO- debug code - delete
        server.del("/discussion/DEL/deleteAll", () => {
            models.sequelize.query('TRUNCATE t_personal_messages;')
                .success((result: any) => {
                    console.log(result);
                    models.sequelize.query('TRUNCATE t_viewed_messages;')
                        .success((result: any) => {
                            console.log(result);
                            models.sequelize.query('SET foreign_key_checks = 0;')
                                .success((result: any) => {
                                    console.log(result);
                                    models.sequelize.query('TRUNCATE t_messages;')
                                        .success((result: any) => {
                                            console.log(result);
                                            models.sequelize.query('SET foreign_key_checks = 1;')
                                                .success((result: any) => {
                                                    console.log(result);
                                                })
                                                .error((error: Error) => {
                                                    console.log(error);
                                                });
                                        })
                                        .error((error: Error) => {
                                            console.log(error);
                                            models.sequelize.query('SET foreign_key_checks = 1;')
                                                .success((result: any) => {
                                                    console.log(result);
                                                })
                                                .error((error: Error) => {
                                                    console.log(error);
                                                });
                                        });
                                })
                                .error((error: Error) => {
                                    console.log(error);
                                });
                        })
                        .error((error: Error) => {
                            console.log(error);
                        });
                })
                .error((error: Error) => {
                    console.log(error);
                });
        });
        //--

        server.listen(port, () => {
            console.log('%s listening at %s', server.name, server.url);
        });
    }

    private generateToken(): string {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            var r: number = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }

    private getTokenFromSession(req: restify.Request): string {
        var cookies = (<any>req).cookies;
        if (cookies && cookies.token) {
            return cookies.token;
        }

        return null;
    }

    private getUserFromToken(token: string, getUserSuccess: (user: Mapparola.Core.User) => void, getUserError: () => void): void {
        if (token) {
            models.t_accounts.find(
                {
                    where: <types.t_accountPojo> {
                        token: token
                    },
                    attributes: ['username']
                }).success((accountInstance: types.t_accountInstance) => {
                    if (accountInstance == null) {
                        console.log("User doesn't exist");
                        return null;
                    } else {
                        console.log("User exists");
                        var foundUser = new core.User();
                        foundUser.Username = accountInstance.values.username;
                        if (getUserSuccess) {
                            getUserSuccess(foundUser);
                        }
                    }
                }).error((err: Error) => {
                    console.log("Error in performing query, error details: ", err);
                    if (getUserError) {
                        getUserError();
                    }
                });
        } else {
            getUserError();
        }
    }

    private isAuthorized(token: string): boolean {
        return !!token;

        //if (this.getUserFromToken(token)) {
        //    return true;
        //}

        //return false;
    }

    private CreateUser(user: Mapparola.Core.User) {
        models.t_accounts.create(
            <types.t_accountPojo>{
                token: this.generateToken(),
                username: user.Username,
                password: user.Password
            }
            ).success((accountInstance: types.t_accountInstance) => {
                if (accountInstance == null) {
                    console.log("Error in creating new user.");
                } else {
                    console.log("User " + user.Username + "/" + user.Password + "created!");
                }
            }).error((err: Error) => {
                console.log("Error in performing query, error details: ", err);
            });
    }

    public LogUserIn(logInArgs: Mapparola.Core.LoginArgs, res: restify.Response) {
        var user = new core.User();

        user.Username = logInArgs.Username;
        user.Password = logInArgs.Password;

        console.log("Log in request received for username '" + user.Username + "' and password '" + user.Password + "'");

        models.t_accounts.find(
            {
                where: <types.t_accountPojo> {
                    username: user.Username,
                    password: user.Password
                },
                attributes: ['token']
            }).success((accountInstance: types.t_accountInstance) => {
                if (accountInstance == null) {
                    console.log("User doesn't exist, creating one.");
                    //this.CreateUser(user);
                    res.send(401);
                } else {
                    console.log("User exists (token: " + accountInstance.values.token + "). Logging in!");
                    //res.header('Access-Control-Allow-Credentials', true);
                    //res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,PATCH');
                    //res.header('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept');

                    var exdate = new Date();
                    exdate.setDate(exdate.getDate() + 365);

                    (<any>res).setCookie(
                        'token',
                        accountInstance.values.token,
                        {
                            "path": '/',
                            "expires": exdate,
                            "maxAge": 60 * 60 * 24 * 365 //1y
                        }
                        );
                    res.send(200, "OK");
                }
            }).error((err: Error) => {
                console.log("Error in performing query, error details: ", err);
                res.send(500);
            });
    }

    public SaveDiscussion(discussion: Mapparola.Core.Discussion, accountToken: string, res: restify.Response): void {
        //TODO - Add a new message to the both the viewed messages and personal messages

        if (discussion && discussion.Location.y && discussion.Location.x && discussion._text && accountToken) {
            var isoDate : string = new Date().toISOString().slice(0, 19).replace('T', ' ').toString();
            var lat: number = discussion.Location.y;
            var lng: number = discussion.Location.x;
            var msg: string = discussion._text;
            var token: string = this.generateToken();

            //TODO - Should we check that this parent/account token really exist or just let the insert into the db fail?
            var parentToken: string = !!discussion.ParentToken ? discussion.ParentToken : null;
            
            models.t_messages.create(
                <types.t_messagePojo>{
                    t_accounts_token: accountToken,
                    created_time: new Date(),
                    token: this.generateToken(),
                    parent: parentToken,
                    message: msg,
                    location: <string><any>models.sequelize.literal("GeomFromText(" + '\'POINT(' + lng + ' ' + lat + ')\'' + ")")
                })
                .success((messageInstance: types.t_messageInstance) => {
                    if (messageInstance == null) {
                        console.log("Error in creating new message.");
                        res.send(500);
                    } else {
                        console.log("Message " + msg + " created!");
                        res.send(200);
                    }
                }).error((err: Error) => {
                    console.log("Error in performing query, error details: ", err);
                    res.send(500);
                });
        } else {
            console.log("Something is missing from message to save.");
            res.send(400);
        }
    }

    private GetAllMessagesAroundPosition(location: Mapparola.Core.Point) {
        if (location.y && location.x) {
            var lat: number = location.y;
            var lng: number = location.x;

            models.t_messages.findAll(
                {
                    where: <string><any>models.sequelize.literal('MBRContains( LineString ( Point ( ' + lng + ' + 5 / ( 111.1 / COS(RADIANS(' + lat + '))), ' + lat + ' + 5 / 111.1), Point ( ' + lng + ' - 5 / ( 111.1 / COS(RADIANS(' + lat + '))), ' + lat + ' - 5 / 111.1) ), location)'),
                    attributes: ['message']
                }).success((messageInstances: types.t_messageInstance[]) => {
                    if (messageInstances == null) {
                        console.log("No messages in around position");
                        //res.send(500);
                    } else {
                        console.log(messageInstances.length + "messages found: ");
                        console.log(messageInstances.forEach(
                            (message: types.t_messageInstance, index: number, array: types.t_messageInstance[]): void => {
                                console.log(message.values.message);
                            }));
                    }
                }).error((err: Error) => {
                    console.log("Error in performing query, error details: ", err);
                    //res.send(500);
                });
        } else {
            console.log("Something is missing from message to get.");
            //res.send(400);
        }
    }

    public GetUserNewMessagesAroundPosition(location: Mapparola.Core.Point, accountToken: string, res: restify.Response) {

        if (location && location.y && location.x) {
            var lat: number = location.y;
            var lng: number = location.x;

            models.t_messages.sequelize.query('SELECT * FROM  t_messages WHERE MBRContains( LineString ( Point ( ' + lng + ' + 5 / ( 111.1 / COS(RADIANS(' + lat + '))), ' + lat + ' + 5 / 111.1), Point ( ' + lng + ' - 5 / ( 111.1 / COS(RADIANS(' + lat + '))), ' + lat + ' - 5 / 111.1) ), location)' +
                'AND token NOT IN ( SELECT t_messages_token FROM mapparola_model.t_viewed_messages WHERE t_accounts_token = "' + accountToken + '")')
                .success((messageInstances: types.t_messageInstance[]) => {
                    if (!messageInstances) {
                        console.log("No messages found");
                    } else {
                        console.log(messageInstances.length + "messages found: ");
                        console.log(messageInstances.forEach(
                            (messageInstance: types.t_messageInstance, index: number, array: types.t_messageInstance[]): void => {
                                console.log(messageInstance.message);
                            }));

                        var accountsToken = Enumerable
                            .from(messageInstances)
                            .select((messageInstance: types.t_messageInstance) => messageInstance.t_accounts_token)
                            .distinct()
                            .toArray();

                        var parentsReturned = Enumerable
                            .from(messageInstances)
                            .where((messageInstance: types.t_messageInstance) => messageInstance.parent === null)
                            .select((messageInstance: types.t_messageInstance) => messageInstance.token).toArray();

                        var incompleteDiscussionsParentTokens = Enumerable
                            .from(messageInstances)
                            .where((messageInstance: types.t_messageInstance) => messageInstance.parent !== null && parentsReturned.indexOf(messageInstance.parent) === -1)
                            .select((messageInstance: types.t_messageInstance) => messageInstance.parent).toArray();

                        var completeDiscussionAvailable = Enumerable
                            .from(messageInstances)
                            .where((messageInstance: types.t_messageInstance) => (incompleteDiscussionsParentTokens.indexOf(messageInstance.token) === -1 && messageInstance.parent === null) || (messageInstance.parent !== null && incompleteDiscussionsParentTokens.indexOf(messageInstance.parent) === -1)).toArray();

                        models.t_messages.findAll(
                            {
                                where: models.sequelize.or(
                                    models.sequelize.and(
                                        {
                                            token: incompleteDiscussionsParentTokens
                                        },
                                        {
                                            parent: null
                                        }),
                                    {
                                        parent: incompleteDiscussionsParentTokens
                                    })
                            }).success((messageInstances: types.t_messageInstance[]) => {
                                if (messageInstances == null) {
                                    console.log("No messages in around position");
                                } else {
                                    // Rebuild discussion history
                                    var unformattedDiscussions: types.t_messagePojo[] =
                                        Enumerable
                                            .from(messageInstances)
                                            .select((messageInstance: types.t_messageInstance) => messageInstance.values).toArray()
                                            .concat(completeDiscussionAvailable);

                                    // From a flattened list of messages we try to recreate the discussion by 
                                    // grouping each messages based on the parent token
                                    var formattedDiscussions: types.t_messagePojo[][] =
                                        Enumerable
                                            .from(unformattedDiscussions)
                                            .where((messageInstance: types.t_messagePojo) => messageInstance.parent === null)
                                            .select((message: types.t_messagePojo) =>
                                                [message].concat(
                                                    <types.t_messagePojo[]>
                                                    Enumerable
                                                        .from(unformattedDiscussions)
                                                        .where((mi: types.t_messagePojo) => mi.parent === message.token).toArray())
                                                        // Order from the oldest to the newest
                                                        .sort((m1: types.t_messagePojo, m2: types.t_messagePojo): number => {
                                                            if (m1.created_time > m2.created_time) {
                                                                return 1;
                                                            }
                                                            return -1;
                                                        })
                                            ).toArray()
                                            // Order from the newest to the oldest
                                            .sort((m1: types.t_messagePojo[], m2: types.t_messagePojo[]): number => {
                                                if (m1[0].created_time > m2[0].created_time) {
                                                    return -1;
                                                }
                                                return 1;
                                            });

                                    var discussions: Mapparola.Core.Discussion[] = [];

                                    formattedDiscussions.forEach((value: types.t_messageInstance[]) => {
                                        var discussion = core.Discussion.FromDatabaseObject(value[0]);

                                        discussion.Responses = [];

                                        for (var i = 1; i < value.length; i++) {
                                            discussion.Responses.push(core.Discussion.FromDatabaseObject(value[i]));
                                        }

                                        discussions.push(discussion);
                                    });

                                    var accountTokenToUsername: { [token: string]: string } = {};

                                    // Gather the information about the usernames based on the account tokens of the message
                                    models.t_accounts.findAll(
                                        {
                                            where: <types.t_accountPojo> {
                                               token: <any>accountsToken
                                            },
                                            attributes: ['username', 'token']
                                        }).success((accountInstances: types.t_accountInstance[]) => {
                                            if (accountInstances == null) {
                                                res.send(500);
                                            } else {
                                                accountInstances.forEach((accountInstance: types.t_accountInstance) => {
                                                    accountTokenToUsername[accountInstance.values.token] = accountInstance.values.username;
                                                });

                                                discussions.forEach((discussion: Mapparola.Core.Discussion) => {
                                                    discussion.Username = accountTokenToUsername[discussion.AccountToken];
                                                    delete discussion.AccountToken;
                                                });

                                                //We finally did it. There you go, here the discussions.
                                                res.send(200, discussions);
                                            }
                                        }).error((err: Error) => {
                                            res.send(500);
                                        });
                                }
                            }).error((err: Error) => {
                                console.log("Error in performing query, error details: ", err);
                                res.send(500);
                            });
                    }
                }).error((err: Error) => {
                    console.log("Error in performing query, error details: ", err);
                    res.send(500);
                });
        } else {
            console.log("Something is missing from message to get.");
            res.send(400);
        }
    }

    public AddToUserViewedDiscussions(discussionToken: string, accountToken: string, res: restify.Response) {

        if (discussionToken && accountToken) {
            models.t_viewed_messages.create(
                <types.t_viewed_messagePojo>{
                    t_accounts_token: accountToken,
                    t_messages_token: discussionToken
                })
                .success((viewedMessageInstance: types.t_viewed_messageInstance) => {
                    if (!viewedMessageInstance) {
                        console.log("Cannot insert viewed message in db");
                        res.send(500);
                    } else {
                        console.log("AccountToken/MessageToken: " + accountToken + "/" + discussionToken + " added to viewed!");
                        res.send(200);
                    }
                }).error((err: Error) => {
                    console.log("Error in performing query, error details: ", err);
                    res.send(500);
                });
        } else {
            res.send(400);
        }
    }

    public AddToPersonalDiscussions(discussionToken: string, accountToken: string, res: restify.Response) {

        if (discussionToken && accountToken) {
            models.t_personal_messages.create(
                <types.t_personal_messagePojo>{
                    t_accounts_token: accountToken,
                    t_messages_token: discussionToken
                })
                .success((personalMessageInstance: types.t_personal_messageInstance) => {
                    if (personalMessageInstance == null) {
                        console.log("Cannot insert personal message in db");
                        res.send(500);
                    } else {
                        console.log("AccountToken/MessageToken: " + accountToken + "/" + discussionToken + " added to personal!");
                        res.send(200);
                    }
                }).error((err: Error) => {
                    console.log("Error in performing query, error details: ", err);
                });
        } else {
            res.send(400);
        }
    }

    public GetAllPersonalMessages(accountToken: string, res: restify.Response) {
        if (accountToken) {
            //First get all the personal message tokens for a given account
            models.t_personal_messages.findAll(
                {
                    where: <types.t_personal_messagePojo>{
                        t_accounts_token: accountToken
                    },
                    attributes: ["t_messages_token"]
                }).success((personalMessageInstances: types.t_personal_messageInstance[]) => {
                    if (!personalMessageInstances) {
                        console.log("No personal messages");
                        res.send(200);
                    } else {
                        var messageTokens: string[] = [];
                        personalMessageInstances.forEach(
                            (personalMessageInstance: types.t_personal_messageInstance, index: number, array: types.t_personal_messageInstance[]): void => {
                                messageTokens.push(personalMessageInstance.values.t_messages_token);
                            });

                        //Then for each of the personal message tokens get the actual message
                        //TODO: access the parent and reconstruct discussion?
                        models.t_messages.findAll(
                            {
                                where: <types.t_messagePojo> {
                                    token: <any>messageTokens
                                }
                            }).success((messageInstance: types.t_messageInstance[]) => {
                                if (!messageInstance) {
                                    console.log("Cannot get personal messages");
                                    res.send(500);
                                } else {
                                    console.log("Got personal messages");
                                    res.send(200);
                                }
                            }).error((err: Error) => {
                                console.log("Error in performing query, error details: ", err);
                                res.send(500);
                            });
                    }
                }).error((err: Error) => {
                    console.log("Error in performing query, error details: ", err);
                    res.send(500);
                });
        } else {
            res.send(400);
        }
    }

}

var server = new ServerApp();

//AccountToken = 'e142a870-ad60-4e9a-8a72-62c747d4de58';
//x = -123.391995;
//y = 48.431855;
